/*
 * Copyright 2007 Tim Peierls
 * Reformatted and minimally adapted by Mathias Doenitz in 2008
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package listo.utils.guice;

import com.google.inject.Key;
import com.google.inject.Provider;
import listo.utils.logging.Log;
import org.apache.commons.lang.builder.ToStringBuilder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Partial implementation of {@link ContextScope}. Concrete implementations
 * must pass the context identifier type to the super constructor and define
 * {@code get()} to return the current context identifier (and to return null
 * or throw an exception if there is no current context). They must also implement
 * the {@link ContextRegistry} interface.
 *
 * @author Tim Peierls [tim at peierls dot net]
 */
public abstract class AbstractContextScope<C, R> implements ContextScope<C>, ContextRegistry<C, R> {

    private enum State {
        OPEN, CLOSED
    }

    protected final Log log;
    private final Class<C> type;
    private final String scopeName;
    private final List<Key<?>> scopedKeys = Collections.synchronizedList(new ArrayList<Key<?>>());
    private final ConcurrentMap<C, State> contexts = new ConcurrentHashMap<C, State>();

    protected AbstractContextScope(Log log, Class<C> type, String scopeName) {
        this.log = log;
        this.type = type;
        this.scopeName = scopeName;
    }

    public String toString() {
        return scopeName;
    }

    public List<Key<?>> getKeysInScope() {
        synchronized (scopedKeys) {
            return new ArrayList<Key<?>>(scopedKeys);
        }
    }

    public C get() {
        return get(null); // override the Provider.get() to delegate to our get(key)
    }

    public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
        log.debug("scope %s: adding key %s with creator %s", scopeName, key, creator);

        scopedKeys.add(key);
        final String name = key.toString();
        return new Provider<T>() {
            public T get() {
                log.debug("scope %s: getting key %s", scopeName, key);

                C context = getContext(key);
                R registry = registryFor(context);
                InstanceProvider<T> future = AbstractContextScope.this.get(registry, key, name);
                if (future == null) {
                    InstanceProvider<T> futureTask = new FutureTaskProvider<T>(creator);
                    future = putIfAbsent(registry, key, name, futureTask);
                    if (future == null) {
                        log.debug("scope %s: creating instance for key %s", scopeName, key);
                        future = futureTask;
                        futureTask.run();
                        if (Thread.currentThread().isInterrupted()) {
                            remove(registry, key, name, futureTask);
                        }
                    }
                }
                return future.get();
            }

            public String toString() {
                return new ToStringBuilder(this)
                        .append("scopeName", scopeName)
                        .append("type", type)
                        .append("key", key)
                        .append("creator", creator)
                        .toString();
            }
        };
    }

    public Class<C> type() {
        return type;
    }

    public Collection<C> getOpenContexts() {
        Collection<C> openContexts = new ArrayList<C>();
        for (C context : contexts.keySet()) {
            if (contexts.get(context) == State.OPEN) {
                openContexts.add(context);
            }
        }
        return openContexts;
    }

    public void close(C context, ContextCloseHandler<?>... closeHandlers) {
        if (!contexts.replace(context, State.OPEN, State.CLOSED)) {
            // Context hadn't been opened or was already closed.
            return;
        }

        for (InstanceProvider<?> provider : registeredProviders(registryFor(context))) {
            Object value = null;
            try {
                value = provider.get();
            }
            catch (RuntimeException e) {
                // Ignore runtime exceptions: they were thrown when
                // attempting creation and mean that no object was
                // created.
            }

            if (value == null) {
                // No instance was created by this provider, so we ignore.
                continue;
            }

            for (ContextCloseHandler<?> closeHandler : closeHandlers) {
                handleClose(closeHandler, value);
            }
        }
    }

    public void closeAll(ContextCloseHandler<?>... closeHandlers) {
        for (C context : getOpenContexts()) {
            close(context, closeHandlers);
        }
    }

    private <T> void handleClose(ContextCloseHandler<T> closeHandler, Object value) {
        Class<T> type = closeHandler.type();
        if (type.isInstance(value)) {
            try {
                closeHandler.close(type.cast(value));
            }
            catch (Exception e) {
                // Ignore exceptions when closing,
                // the closeHandler should have taken
                // appropriate action before rethrowing.
            }
        }
    }

    private C getContext(Key<?> key) {
        C context = null;
        RuntimeException caught = null;
        try {
            context = get(key);
            if (contexts.putIfAbsent(context, State.OPEN) == State.CLOSED) {
                // Context is closed.
                context = null;
            }
        }
        catch (RuntimeException ex) {
            caught = ex;
        }
        if (context == null) {
            throw new OutOfScopeException(this, key, caught);
        }

        return context;
    }

    private Collection<InstanceProvider<?>> registeredProviders(R registry) {
        List<InstanceProvider<?>> providers = new ArrayList<InstanceProvider<?>>();
        for (Key<?> key : getKeysInScope()) {
            InstanceProvider<?> provider = get(registry, key, key.toString());
            if (provider != null) {
                providers.add(provider);
            }
        }
        return providers;
    }

}

